It is currently 2:00 A.M., and you have restarted Jenkins twice in an attempt to build your large-scale Maven multilanguage project. However, you are still getting out of memory errors, “Java Heap Space” or “PermGen Space.” I understand how frustrating this can be. After restarting Jenkins and attempting to troubleshoot an issue, you may be contemplating whether there is something “cursed” within your CI pipeline. After years of dealing with this same issue and finding a resolution through trial-and-error, I can tell you that in most cases, the cause of this issue is due to a configuration error within the Jenkins settings, not because of a hidden bug within the Maven application code. This guide will help you understand how to fix these issues quickly so that you can continue building your projects successfully.
Quick Summary
- It is almost always true that an out of memory error does not mean you should purchase more RAM. Rather, it means the maximum heap size allowed by the Java Virtual Machine (JVM) for the Jenkins controller/agent’s process is too low for the required Maven build process.
- An out-of-memory error can occur in two different places: the Jenkins controller and the build agent. They each have their own unique method of utilizing memory and causing OOM errors.
- Making a single line change to the MAVEN_OPTS environment variable in your Jenkinsfile can resolve this issue for an individual project; however, the presence of a resource leak in a multibranch pipeline may cause you issues down the road.
- Going forward, implementing a more up-to-date garbage collection strategy along with real-time monitoring will protect you from continually reconfiguring the heap sizes in your Jenkins setup.
Understanding the jenkins java.lang.OutOfMemoryError java heap space maven Error
Your Maven build is executed by a JVM with a maximum heap size setting that cannot be modified after the build has started (specified using the -Xmx flag).If the sum of all the objects on the heap plus the garbage that is not yet collected exceeds this ceiling, the JVM will issue a java.lang.OutOfMemoryError: Java heap space exception. The final outcome in Jenkins is that the entire step fails with a red ball. The difficult part is determining which JVM is failing: the controller that handles the job or the agent that actually runs the mvn command.
Identifying the Crash Signals
The first step to diagnosing a failed build is to examine the Jenkins console output produced during the failed build. The output will usually contain a stack trace generated by the Maven build during the compile or test stage. You may see a stack trace with {java.lang.OutOfMemoryError: Java heap space} somewhere in it, for example:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project huge-app: Fatal error compiling: java.lang.OutOfMemoryError: Java heap space <--
[ERROR] -----------------------------------------------------
[ERROR] realm = plugin>org.apache.maven.plugins:maven-compiler-plugin:3.11.0
[ERROR] strategy = org.codehaus.plexus.classworlds.strategy.SelfFirstStrategy
[ERROR] url[0] = file:/var/jenkins_home/.m2/repository/org/apache/maven/plugins/maven-compiler-plugin/3.11.0/maven-compiler-plugin-3.11.0.jar
...
Caused by: java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3745)
at java.base/java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:120)
at java.base/java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:95)
at java.base/java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:145)
at com.sun.tools.javac.util.ByteBuffer.appendStream(ByteBuffer.java:208)
The <-- indicates where the heap memory limit was reached — the JVM that was used by the compiler (which was forked from Maven) ran out of heap space. If you see the error listed at the very top of the stack trace coming from hudson.model.Run or jenkins.model.Jenkins, then the problem is with the Jenkins controller. It is important to note who is throwing the exception and to use the appropriate path during the fix.
Prerequisites for Troubleshooting
Make sure to document the exact Java and Maven versions you are using for your build prior to making any changes. To capture these details from within a pipeline step or from the agent node, use the following commands:
$ java -version
openjdk version "17.0.9" 2023-10-17 LTS
OpenJDK Runtime Environment (build 17.0.9+8-LTS)
OpenJDK 64-Bit Server VM (build 17.0.9+8-LTS, mixed mode, sharing)
$ mvn -v
Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
Maven home: /opt/apache-maven-3.9.6
Java version: 17.0.9, vendor: Eclipse Adoptium, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-91-generic", arch: "amd64", family: "unix"
This information will allow you to confirm whether your builds are using a small heap size by default (for example, if they are using a server JRE with a maximum heap size of 256 megabytes) and whether you are using a JDK version that supports modern garbage collectors.
Analyzing Root Causes: jenkins controller vs agent memory limits
Many people see “Jenkins” in the error message and immediately assume that they need to increase the controller’s heap size.In some instances, this strategy can be beneficial, however in many cases the controller only provides feedback from the unresponsive agent. I have spent hours tuning the wrong JVM.
Controller Memory Overload
The Jenkins controller (formerly known as the ‘master’) contains all job configuration and build logs within its memory and performs pipeline execution graph management. If you are processing many jobs and/or a very large multi branch pipeline with extensive history, you may find that the controller’s heap has filled; additionally, the controller typically does not run Maven. Therefore, if the OOM stack contains either ‘hudson.model.Queue’ or ‘jenkins.triggers’, the problem is often related to exceeding memory limits for the controller, most likely due to too many build records or not enough memory for parsing the DSL of a pipeline.
Agent-Level Heap Exhaustion
While a significant number of instances of this type of failure occur on the controller, far more often such failures occur on the build agent JVM which spawns the Maven process. The build agent uses its -Xmx setting, defined in the Jenkins agent startup script or within the service file. Unless you modified the default setting, the value is frequently 256m or 512m. For any large multi-module application project, there will be a huge amount of overhead as the Maven build process will use an above-average amount of memory during the parsing of all source files when loading the compiler’s abstract syntax trees. Additionally, during the build process, Maven forking will create a separate Compiler Daemon that will be using its own memory (MAVEN_OPTS or the Maven plugin configuration). This Compiler Daemon can have a separate -Xmx value.
What Did Not Work for Me
To illustrate, the first time that I changed the -Xmx to 4g on the controller and restarted, I was still having build failures because the build agent which is running Maven was still set to 512m.Lesson: Be sure to determine where the stack trace is coming from. After setting both the agent’s heap to 2g and adding MAVEN_OPTS explicitly in the pipeline, the OOM issue was resolved.
Immediate Resolutions for Large Maven Builds
If you need your builds to be successful immediately, make the appropriate limits to where it makes a difference.
How to increase jenkins heap size xmx globally
To increase the heap on the Jenkins controller (if the controller is slow or OOM), increase the controller’s heap size. If the agents are being OOM’d, increase the agent JVM size. The way to do this depends on how you started Jenkins.
For Systemd agents (most commonly found on Linux nodes), you will either adjust the service drop-in or the environment file as follows:
[Service]
Environment="JAVA_OPTS=-Xms512m -Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/tmp/jenkins-agent-oom.hprof"
After you make the changes, restart the agent with systemctl daemon-reload && systemctl restart jenkins-agent. The setting of -Xmx2048m gives the agent 2GB of Heap space. Remember to set the heap dump options for your future analysis.
The above code increases the JVM memory on the agent to 2GB, and if it still crashes, a heap dump will be created for further investigation. If your Jenkins controller is running via the war file, you can set JAVA_OPTS as indicated in the Jenkins administration guide.
Setting maven_opts jenkins pipeline variables
Even though you may have a much larger heap size on your agent JVM, if the Maven compiler fork is started, it may still only get a small heap. You can fix that by setting MAVEN_OPTS in the environment variables of your pipeline. I add this variable in every heavy Jenkinsfile that I create:
pipeline {
agent any
environment {
MAVEN_OPTS = '-Xms256m -Xmx1536m -XX:+UseG1GC'
}
stages {
stage('Build') {
steps {
sh 'mvn clean install -DskipTests'
}
}
}
}
This sets the heap for all JVM processes spawned by Maven (including the compiler) to be 1.5GB and helps prevent the OOM problems encountered earlier. It is best practice to define MAVEN_OPTS separately from JAVA_OPTS, so you do not inadvertently create a bloated agent configuration.Under MAVEN_OPTIONS, the options used to run mvn commands are defined by the choices made when setting this up.
Edge Case: multibranch pipeline memory leak Detection
Some builds will have a slow increase in the heap over the course of many different builds via the leak process and will continue until the heap is full. The same may occur in cases where there are multiple build pipelines operating simultaneously using very aggressive branch indexing with large sh steps. In these situations, the build pipelines can cause ClassLoaders or BuildFlow objects to remain in memory after the completion of a given build. With this type of issue, it is possible that you could see a constant increase in the amount of memory being used on the agent and an Out of Memory (OOM) situation after several days of uptime on the Jenkins agent.
Capturing and extracting the heap dump
Once you have determined that there is an increase in the amount of memory being used, it is essential to capture a heap dump at the time that the memory fill-up occurs, which is before a crash actually happens. Using jcmd within the agent JVM, you will need to execute the following command to generate the heap dump:
$ jcmd $(pgrep -f agent.jar) GC.heap_dump /tmp/jenkins-leak.hprof
Heap dump file created
The above command will generate a complete heap dump in a file format. If you have set the value of the JVM option -XX:+HeapDumpOnOutOfMemoryError, the generated heap dump will be located in the path that you specified. Regardless of your configuration, it is essential to get the generated heap dump file (typically .hprof) from the build agent to your own workstation (laptop/computer) as soon as possible.
How to analyze jenkins heap dump data
To analyze the heap dump produced by Jenkins, open the heap dump in Eclipse Memory Analyzer (MAT). Run the report titled: “Leak Suspects Report” (Reports > Leak Suspects). The report will provide the retained sizes and identify the largest amount of accumulated data. In a standard multibranch pipeline case, the report might look something similar to this:
One instance of "org.jenkinsci.plugins.workflow.cps.CpsFlowExecution" loaded by "org.eclipse.jetty.webapp.WebAppClassLoader" occupies 482,315,432 (89.74%) bytes. <-- Leak Suspect
The memory is accumulated in one instance of "java.util.LinkedHashMap$LinkedEntry[]" loaded by "system class loader".
The area with a “<–” annotation identifies the leaking object.In this instance, we find that older executions of a pipeline are not being removed. It could be due to an incomplete build discarding configuration on the multibranch pipeline or there is potentially a plugin that is still utilizing references to that old pipeline execution. In order to fix this permanently, either the plugin must be updated, or you must create a periodic job to remove stale runs. The provided heap dump confirms that you are not losing your mind.
Long-Term Stability and jenkins garbage collection tuning
When you have successfully dealt with the immediate OOM, you want your JVM to handle memory spikes without constantly having to adjust the -Xmx setting. This is where garbage collector tuning can come into play.
Switching to G1GC or Shenandoah
The default serial or parallel garbage collectors can create long pause times and the inability to recover most of the space when under heavy build loads. Therefore, G1GC would be an excellent starting point for garbage collection tuning. I generally recommend to add the following flags to your agent’s Java Options (and possibly your controller as well):
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+ParallelRefProcEnabled
By adding the above flags to the agent, you are telling the JVM to utilize the G1 collector and aim to provide very short pause times so that your pipeline continues to respond during builds. If your build uses JDK 17 or newer and you want even less pause time in your builds, you could also switch to Shenandoah by including the additional flag: -XX:+UseShenandoahGC. However, be sure to test Shenandoah because some older plugins have been known to behave strangely with it.
Monitoring Memory Allocation Real-Time
You cannot tune what you cannot see. To see all memory allocation activity, you need to open VisualVM on the agent machine and connect it to the local Agent JVM. Once connected, click on the Monitor Tab and watch the Heap Graph as a build is running. You will see the memory usage spike during the compile phase and subsequently drop back down once the Maven Process that started it has exited. If you do not see the heap drop back down to its baseline levels, either there is a leak in that pipeline or the JVM is not performing collections fast enough. By simply watching the Heap Graph, I have caught numerous misconfigured pipelines while indexing branches.
Frequently Asked Questions
Why does my build fail even after increasing the heap size?
You could be encountering a different memory area. If you receive an error like OutOfMemoryError: Metaspace or Compressed Class Space, this means that the Metaspace is full due to the number of classes that you have loaded into memory. To fix this you will need to increase the maximum Metaspace Size (ex: -XX:MaxMetaspaceSize=256m). Additionally, you should inspect your operating system for per-process memory limit due to cgroups; for instance, if your Jenkins Agent is in a Docker container, the container may impose limitations on the agent that will override the -Xmx setting.
Does allocating too much memory to Jenkins cause performance issues?
Yes, over tuning memory allocation can severely impact performance due to the length of time it takes to scan through such a large heap when performing garbage collection. If the sum of the agents heap plus the combined heap usages from everything else running on the Operating System is greater than the amount of physical RAM in your machine, you are going to swap out pages which will degrade performance. A good starting point for an agent’s memory would be to allocate somewhere between 2-4 GB and monitor this value and adjust as needed.
How can I isolate memory settings for a single specific Maven project?
To allocate memory specifically to a single Maven Project within a Jenkinsfile, you could set MAVEN_OPTS in the environment block of that specific Jenkinsfile per project. This will override the global setting for that agent only. If you require the same MAVEN_OPTS setting individually for each mvn command being executed for a particular job, then supporting that functionality is accomplished using the withEnv block around each individual mvn command execution. No provisions need to be made to agent-wide or controller-wide flags.